defmodule CypherLib do
    @moduledoc ""

    import SyntaxMacro
    import ErrCode
    @struct_field :__struct__


    # %{}  =>  (node:LABEL{})
    def insert_cypher(struct) when Kernel.is_struct(struct) do
        mod = Map.get(struct, @struct_field)
        label = mod.label()
        alias = mod.alias()

        str = if alias == nil do
            "(#{String.downcase(label)}:#{label}"
        else
            "(#{String.downcase(label)}:#{label}:#{alias}"
        end

        str <> kv_cypher(MapLib.drop_key(struct, [@struct_field])) <> ")"
    end

    def insert_cypher(mod, args) do
        mod.new(args)
        |> insert_cypher()
    end

    def update_cypher(node_name, struct) do
        MapLib.drop_key(
            struct,
            [:__struct__, :imgs, :visible]
        )
        |> update_cypher_(node_name)
    end

    def update_cypher(node_name, mod, args) do
        update_cypher(node_name, mod.new(args))
    end

    def update_cypher_(kvs, node_name) do
        Enum.map_join(kvs, ", ", &("#{node_name}.#{elem(&1, 0)} = #{inspect(to_str(elem(&1, 1)))}"))
    end

    def kv_cypher(nil) do
        ""
    end
    def kv_cypher(kvs) do
        Enum.map_join(kvs, ", ", &("#{elem(&1, 0)}: #{inspect(to_str(elem(&1, 1)))}"))
        |> case do
               "" ->
                   ""
               str ->
                   "{#{str}}"
           end
    end
    def kv_cypher(kvs, constant) do
        Enum.map_join(
            kvs,
            ", ",
            fn ({key, value}) ->
                case Map.get(constant, key) do
                    nil ->
                        key <> ": " <> inspect(to_str(value))
                    value1 ->
                        key <> ": " <> value1
                end
            end
        )
        |> case do
               "" ->
                   ""
               str ->
                   "{#{str}}"
           end
    end

    def update_set(kvs, mod, name) do
        str = mod.fields()
              |> Enum.reduce(
                     [],
                     fn
                         {key, %{mod: _mod}}, acc ->
                             acc
                         {key, _}, acc ->
                             case Map.get(kvs, key) do
                                 nil ->
                                     key <> ": " <> name <> "." <> key
                                 val ->
                                     key <> ": " <> inspect(to_str(val), limit: :infinity)
                             end
                             |> append2(acc)
                     end
                 )
              |> Enum.join(", ")
        "{#{str}}"
    end

    # rel_field: 对象有个属性是 关系字段; rel_obj: 关系对象; obj: 关系关联的对象|关系对象(属性是关系)
    def get_obj_rel_attrs(mod, condition, outer_obj, select, {rel_field, rel_obj, obj}) do
        {obj_attr, rel_attr, where} =
            Map.get(mod.fields(), rel_field, %{})
            |> get_rel_attr_cypher_(select, rel_obj, obj)
        str = get_match_where(mod, condition, outer_obj, rel_obj, obj)
        where =
            if str == "" do
                if where == "" do
                    ""
                else
                    " WHERE #{where} "
                end
            else
                if where == "" do
                    str
                else
                    str <> " AND #{where} "
                end
            end
        {obj_attr, rel_attr, where}
    end

    def get_rel_attr_cypher_(rel_fields, select, rel_name, obj_name) do
        {obj_attr, rel_attr, where} = Enum.reduce(
            select,
            {[], [], []},
            &get_rel_attr_cypher_(rel_name, obj_name, select, rel_fields, &1, &2)
        )
        rel_attr =
            if rel_attr == [] do
                ""
            else
                if obj_attr != [] do
                    ", #{rel_attr}"
                else
                    "#{rel_attr}"
                end
            end
        {Enum.join(obj_attr, ", "), rel_attr, iif?(where == [], "", where)}
    end


    def get_rel_attr_cypher_(_rel_name, obj_name, _, _rel_fields, {field, true}, {acc1, acc2, acc3}) do
        {[field <> ": #{obj_name}." <> field | acc1], acc2, acc3}
    end
    def get_rel_attr_cypher_(rel_name, _obj_name, rel_select, rel_fields, {field, info}, {acc1, acc2, acc3}) do
        where = get_condition(Map.get(rel_fields, rel_name), Map.get(info, "()", %{}), rel_name)
        rel_keys = Map.get(info, "=>", %{})
                   |> Enum.map_join(", ", &(elem(&1, 0) <> ": #{rel_name}." <> elem(&1, 0)))
        str = "#{field}: {" <> rel_keys <> "}"
        {acc1, [str | acc2], iif?(where == "", acc3, [where | acc3])}
    end

    def get_match_where(mod, outer_where, left_obj, rel_obj, right_obj) do
        all_fields = mod.fields()
        if Kernel.map_size(outer_where) == 0 do
            ""
        else
            " WHERE " <> Enum.map_join(
                outer_where,
                " AND ",
                fn
                    ({"name", value}) -> # 查询该对象上面的条件
                        if is_map(value) do
                            left_obj <> ".name CONTAINS(#{inspect(Map.get(value, "value"))})"
                        else
                            if is_list(value) do
                                left_obj <> ".name IN #{inspect(value)}"
                            else
                                left_obj <> ".name = #{inspect(value)}"
                            end
                        end
                    ({field, value}) when is_map(value) ->
                        case Map.get(value, "value") do
                            nil ->
                                case Map.get(all_fields, field) do
                                    nil ->
                                        throw({:error, cypher_input_error()})
                                    %{:mod => rel_mod} -> # 关系的对象上的条件
                                        get_condition(rel_mod.fields(), value, right_obj)
                                    rel_fields -> # 关系的条件
                                        get_condition(rel_fields, value, rel_obj)
                                end
                            v ->
                                left_obj <> ".#{field} CONTAINS(#{inspect(v)})"
                        end
                    ({field, value}) -> # 查询该对象上面的条件
                        get_field_condition(mod, left_obj, field, value)
                end
            )
        end
    end

    def get_condition(mod, condition, node_name) when is_atom(mod) do
        mod.fields()
        |> get_condition(condition, node_name)
    end
    def get_condition(fields, condition, node_name) do
        case Kernel.map_size(condition) do
            0 ->
                ""
            _ ->
                Enum.map_join(
                    condition,
                    " AND ",
                    &get_field_condition(fields, node_name, elem(&1, 0), elem(&1, 1))
                )
        end
    end

    def get_field_condition(mod, node_name, field, field_where) when is_atom(mod) do
        Map.get(mod.fields(), field)
        |> Map.get(:type)
        |> field_condition_(node_name, field, field_where)
    end
    def get_field_condition(fields, node_name, field, field_where) when fields != nil do
        Map.get(fields, field)
        |> Map.get(:type)
        |> field_condition_(node_name, field, field_where)
    end
    def get_field_condition(_, _node_name, _field, _field_where) do
        throw({:error, cypher_input_error()})
    end


    def field_condition_(type, node_name, field, field_where) do
        is_map(field_where) && throw({:error, field_where, cypher_input_error()})
        field1 = node_name <> "." <> field
        field_is_list = is_tuple(type) && elem(type, 0) == :array
        cond do
            field_is_list && is_list(field_where) ->
                "ANY(x IN #{field1} WHERE x IN #{inspect(field_where)}) "
            is_list(field_where) ->
                field1 <> " IN " <> inspect(field_where)
            field_is_list ->
                field_where <> " IN " <> inspect(field1)
            true ->
                field_where != nil || throw({:error, field_where, cypher_input_error()})
                field1 <> " = " <> inspect(field_where)
        end
    end


    def dup_unique_key_where(enums, obj) do
        Enum.map_join(
            enums,
            " OR ",
            fn (enum) ->
                "(" <> Enum.map_join(
                           enum,
                           " AND ",
                           &(obj <> "." <> elem(&1, 0) <> " = " <> inspect(elem(&1, 1)))
                       )
                <> ")"
            end
        )
    end

    # type:对象数组, 不同的对象有不同的属性, 用 unwind 遍历 neo4j 里的数组对象
    # unwind [%{"a" => a, "b" => b}] as row, {a: row.a, b: row.b}
    def unwind_row_obj_args(attrs, row, unique_keys) when is_list(attrs) do
        unwind_row_obj_args(hd(attrs), row, unique_keys)
    end
    def unwind_row_obj_args(attr, row, unique_keys) do
        MapLib.drop_key(attr, unique_keys)
        |> Enum.map_join(", ", &(elem(&1, 0) <> ": " <> row <> "." <> elem(&1, 0)))
    end

    # 把 elixir 里的[%{}] =>　neo4j 里的数组对象 [{k: v}]
    def to_map_array(enums) do
        "[" <> Enum.map_join(enums, ", ", &to_map_str(&1)) <> "]"
    end

    def to_map_str(enum) do
        str = enum
              |> Enum.map_join(
                     ", ",
                     fn
                         ({field, value}) when is_map(value) ->
                             Neo4jLib.check_params_valid!(field)
                             Neo4jLib.check_params_valid!(value)
                             field <> ":" <> inspect(Jason.encode!(value))
                         ({field, value}) ->
                             Neo4jLib.check_params_valid!(field)
                             Neo4jLib.check_params_valid!(value)
                             field <> ":" <> inspect(value)
                     end
                 )
        "{" <> str <> "}"
    end


    def to_str(v) do
        case Kernel.is_map(v) do
            true ->
                Jason.encode!(v)
            _ ->
                v
        end
    end

    def ts_ymd(field, alias) do
        "to_char(#{field}, 'yyyy-MM-dd') #{alias}"
    end


    defmacro test() do
        quote unquote: false, location: :keep do
            a = %{
                    __struct__: DeviceNeoDb,
                    available: false,
                    #                    brand: nil,
                    #                    imgs: "{}",
                    #                    maintenance: "{}",
                    #                    model: nil,
                    #                    name: nil,
                    #                    status: nil,
                    #                    type: nil,
                    #                    uid: nil,
                    visible: "{}"
                }
                |> Map.delete(:__struct__)

            args =
                a
                |> Enum.map_join(
                       ", ",
                       fn ({k, _}) ->
                           Atom.to_string(k)
                       end
                   )
            "{" <> args <> "}"
            str = Enum.map_join(
                a,
                ", ",
                fn
                    {:visible, _} ->
                        "visible" <> ": " <> "true"
                    {k, _} ->
                        k = Atom.to_string(k)
                        k <> ": " <> k
                end
            )
            struct = "%{__struct__: #{__MODULE__}, " <> str <> "} |> MapLib.drop_key([\"imgs\"])"
            def new(Macro.var(args)) do
                Macro.escape(struct)
            end
        end
    end



end
